﻿#region License
/* ---------------------------------------------------------------- *
 *
 * This file is part of the Xcoordination Application Space
 * ("XcoAppSpace") http://www.xcoordination.com
 *
 * Copyright (C) 2009 Xcoordination http://www.xcoordination.com
 *
 * XcoAppSpace is free software; you can redistribute it and/or
 * modify it under the terms of version 2 of the GNU General
 * Public License as published by the Free Software Foundation.
 *
 * XcoAppSpace is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see http://www.gnu.org/licenses/
 * or write to the Free Software Foundation, Inc., 51 Franklin
 * Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * ---------------------------------------------------------------- */
#endregion

using System;
using System.Collections.Generic;
using System.Reflection;
using BruteForceSerializer.Contract;
using BruteForceSerializer.Contract.Internal;
using System.Linq;
using BruteForceSerializer.Surrogates;
using TypeResolve;

namespace BruteForceSerializer.Normalization
{
    internal class Normalizer
    {
		public List<ISerializationSurrogate> Surrogates { get; private set; }

		public Normalizer()
		{
			this.Surrogates = new List<ISerializationSurrogate>
			                  	{
			                  		new TypeSurrogate(),
			                  		new ListSurrogate(),
			                  		new DictionarySurrogate(),
			                  		new GuidSurrogate(),
			                  		new ExceptionSurrogate(),
									new ByteArraySurrogate()
			                  	};
		}

        public INormalizedObject Normalize(object objectGraphRoot)
        {
            return (INormalizedObject)Normalize(objectGraphRoot, new Dictionary<object, int>());
        }

        internal object Normalize(object objectGraphRoot, Dictionary<object, int> visitedObjects)
        {
            if (objectGraphRoot == null) return null;

            int objectId;
            if (ObjectIsCached(objectGraphRoot, visitedObjects, out objectId))
                return new NormalizedReference(objectId);

            objectId = CacheObject(objectGraphRoot, visitedObjects);

			var type = GetTypeToSerialize(objectGraphRoot);
			var no = new NormalizedObject(TypeResolver.TypeToString(type, objectGraphRoot), objectId);
            {
            	var surrogate = GetSurrogate(type);
				if (surrogate == null)
				{
					if (type.IsSimpleType())
						no.AddField("value", TransformToValue(type, objectGraphRoot, visitedObjects));
					else if (type.IsArray)
						NormalizeArrayOfObjects(objectGraphRoot, visitedObjects, no);
					else
						NormalizeSingleObject(objectGraphRoot, visitedObjects, no);
				}
				else
				{
					var data = new Dictionary<string, object>();
					surrogate.GetData(objectGraphRoot, data);
					foreach (var item in data)
					{
						no.AddField(item.Key, TransformToValue(item.Value.GetType(), item.Value, visitedObjects));
					}
				}
            }
            return no;
        }

		private static Type GetTypeToSerialize(object objectGraphRoot)
		{
			if (objectGraphRoot is Type)
				return typeof(Type); //ensure that instances of "RuntimeType", which is a non-public class, are serialized as instances of "Type"
			var type = objectGraphRoot.GetType();
			if (type.GetTypeInfo().IsNotPublic)
				throw new ArgumentException("Only public types can be serialized! " + type.Name + " is not public."); //this is so that errors because of non-public classes become immediately visible
			return type;
		}

		private ISerializationSurrogate GetSurrogate(Type type)
		{
			return this.Surrogates.FirstOrDefault(s => s.ApplyOnType(type));
		}

        private static bool ObjectIsCached(object objectGraphRoot, IDictionary<object, int> visitedObjects, out int objectId)
        {
            return visitedObjects.TryGetValue(objectGraphRoot, out objectId);
        }

        private static int CacheObject(object objectGraphRoot, IDictionary<object, int> visitedObjects)
        {
            int objectId = visitedObjects.Count;
            visitedObjects.Add(objectGraphRoot, objectId);
            return objectId;
        }

        private void NormalizeArrayOfObjects(object objectGraphRoot, Dictionary<object, int> visitedObjects, NormalizedObject no)
        {
            int i = 0;
            foreach (object element in (Array)objectGraphRoot)
                no.AddField(/*string.Format("[{0}]", i++)*/(i++).ToString(), TransformToValue((element != null ? element.GetType() : null), element, visitedObjects));
        }

		private void NormalizeSingleObject(object objectGraphRoot, Dictionary<object, int> visitedObjects, NormalizedObject no)
		{
			foreach (var fi in objectGraphRoot.GetType().GetSerializableFields())
				no.AddField(fi.Name, TransformToValue(fi.FieldType, fi.GetValue(objectGraphRoot), visitedObjects));
			foreach (var pi in objectGraphRoot.GetType().GetSerializableProperties())
				no.AddField(pi.Name, TransformToValue(pi.PropertyType, pi.GetValue(objectGraphRoot, null), visitedObjects));
		}

    	private object TransformToValue(Type valueType, object value, Dictionary<object, int> visitedObjects)
        {
			if (value == null) return null;
            if (valueType == typeof(IntPtr)) return ((IntPtr)value).ToInt64();
            if (valueType.IsSimpleType()) return value;
            return Normalize(value, visitedObjects);
        }

		public object DeNormalize(INormalizedObject no)
		{
			return DeNormalize(no, new Dictionary<int, object>());
		}

		internal object DeNormalize(object objectGraphRoot, Dictionary<int, object> visitedObjects)
		{
			if (objectGraphRoot == null)
				return null;
			if (objectGraphRoot is INormalizedReference)
				return GetFromCache((INormalizedReference) objectGraphRoot, visitedObjects);
			if (!(objectGraphRoot is INormalizedObject))
				throw new Exception("INormalizedObject expected.");

			var no = (INormalizedObject) objectGraphRoot;
			int[] arrayDimensions;
			var type = TypeResolver.Resolve(no.Typename, out arrayDimensions);
			//if (type == typeof(string))
			//    return no.Fields.First().Value.ToString();
			if (type.IsSimpleType())
				return type.ConvertValue(no.Fields.First().Value);

			var surrogate = GetSurrogate(type);
			object instance = CreateInstance(type, arrayDimensions, surrogate);
			if (instance != null)
				visitedObjects.Add(no.Id, instance);

			var surrogateData = (surrogate != null ? new Dictionary<string, object>() : null);
			foreach (var field in no.Fields)
			{
				object fieldValue = DeNormalizeField(visitedObjects, field);
				if (surrogateData != null)
					surrogateData.Add(field.Name, fieldValue);
				else if (arrayDimensions == null)
				{
					type.SetSerializableFieldOrPropertyValue(instance, field.Name, fieldValue);
				}
				else
				{
					fieldValue = type.GetElementType().ConvertValue(fieldValue);
					int arrayIndex = int.Parse(field.Name); //field.Name.Substring(1, field.Name.Length - 2));
					((Array) instance).SetValue(fieldValue, arrayIndex);
				}
			}
			if (surrogate != null)
			{
				instance = surrogate.SetData(instance, type, surrogateData);
				visitedObjects[no.Id] = instance;
			}
			return instance;
		}

    	private object DeNormalizeField(Dictionary<int, object> visitedObjects, INormalizedField field)
    	{
    		object fieldValue;
    		if (field.Value is INormalizedObject || field.Value is INormalizedReference)
    			fieldValue = DeNormalize(field.Value, visitedObjects);
    		else
    			fieldValue = field.Value;
    		return fieldValue;
    	}

    	private static object CreateInstance(Type type, int[] arrayDimensions, ISerializationSurrogate surrogate)
    	{
    		object instance;
    		if (surrogate != null)
    			instance = surrogate.CreateInstance(type); //arrayType ?? type);
    		else
    		{
				//try
				//{
    				instance = (arrayDimensions != null
    				            	? Array.CreateInstance(type.GetElementType(), arrayDimensions)
    				            	: Activator.CreateInstance(type));
				//}
				//catch (Exception ex)
				//{
				//	Console.WriteLine("Error instantiating type " + type + ". Message: " + ex.Message);
				//	throw;
				//}
    		}
    		return instance;
    	}

    	

    	private static object GetFromCache(INormalizedReference noRef, IDictionary<int, object> visitedObjects)
    	{
    		if (visitedObjects.ContainsKey(noRef.ObjectId))
    			return visitedObjects[noRef.ObjectId];
    		throw new Exception("Object Reference with id " + noRef.ObjectId + " not found.");
    	}
    }
}
